TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { notFound } from 'next/navigation';4import { publicProfile } from '@/lib/account/queries';5import { Card, CardHeader, Delta, Table, th, td, tdNum } from '@/components/ui/primitives';6import { Donut } from '@/components/account/charts';7import { ValueSourceBadge } from '@/components/account/portfolio-widgets';8import { fmtMoney, confidenceLabel } from '@/lib/format';910export async function generateMetadata({ params }: { params: Promise<{ handle: string; slug: string }> }): Promise<Metadata> {11 const { handle, slug } = await params;12 const p = await publicProfile(handle);13 const c = p?.collections.find((x) => x.collection.publicSlug === slug);14 if (!p || !c) return { title: 'Collection not found' };15 return { title: `${c.collection.name} · ${p.user.name ?? `@${handle}`}`, description: c.collection.description ?? `${c.summary.itemCount} items on RareIndex` };16}1718export default async function PublicCollectionPage({ params }: { params: Promise<{ handle: string; slug: string }> }) {19 const { handle, slug } = await params;20 const p = await publicProfile(handle);21 const c = p?.collections.find((x) => x.collection.publicSlug === slug);22 if (!p || !c) notFound();23 const items = p.items.filter((i) => i.collectionId === c.collection.id);24 const rows = [...c.summary.items].sort((a, b) => (b.valueUsd ?? 0) - (a.valueUsd ?? 0));25 return (26 <div className="mx-auto max-w-5xl">27 <nav className="mb-2 pt-4 text-xs text-muted">28 <Link href={`/u/${handle}`} className="hover:text-fg">29 @{handle}30 </Link>{' '}31 / <span className="text-fg">{c.collection.name}</span>32 </nav>33 <header className="mb-5 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">34 <div>35 <h1 className="text-2xl font-semibold tracking-tight">{c.collection.name}</h1>36 {c.collection.description ? <p className="mt-1 text-sm text-muted">{c.collection.description}</p> : null}37 </div>38 <div className="text-right">39 <p className="num text-2xl font-semibold">{c.summary.valuedCount ? fmtMoney(c.summary.valueUsd) : '—'}</p>40 <p className="text-xs text-muted">41 {c.summary.itemCount} items · confidence {confidenceLabel(c.summary.confidence)}42 </p>43 </div>44 </header>45 <div className="mb-5 grid gap-4 md:grid-cols-3">46 <Card className="md:col-span-1">47 <CardHeader title="Allocation" />48 <div className="p-4">49 <Donut data={c.summary.allocationByCategory.map((b) => ({ label: b.label, value: b.valueUsd }))} size={100} />50 </div>51 </Card>52 <Card className="md:col-span-2">53 <CardHeader title="Items" />54 <Table>55 <thead>56 <tr>57 <th className={th}>Item</th>58 <th className={th}>Variant</th>59 <th className={`${th} text-right`}>Qty</th>60 <th className={`${th} text-right`}>Value</th>61 <th className={th}>Basis</th>62 </tr>63 </thead>64 <tbody>65 {rows.map((i) => {66 const src = items.find((x) => x.id === i.id)!;67 return (68 <tr key={i.id}>69 <td className={td}>70 <div className="flex items-center gap-2.5">71 {src.photos[0] || src.heroImageUrl ? (72 // eslint-disable-next-line @next/next/no-img-element73 <img src={src.photos[0] ?? src.heroImageUrl ?? ''} alt="" className="h-9 w-9 rounded-sm object-cover" />74 ) : (75 <span className="h-9 w-9 rounded-sm bg-inset" />76 )}77 <Link href={`/asset/${src.assetSlug}`} className="max-w-[260px] truncate font-medium hover:underline">78 {i.title}79 </Link>80 </div>81 </td>82 <td className={`${td} text-xs`}>{src.variantLabel ?? (i.grader ? `${i.grader.toUpperCase()} ${i.grade ?? ''}` : src.condition ?? '—')}</td>83 <td className={tdNum}>{i.quantity}</td>84 <td className={tdNum}>{i.valueUsd === null ? <span className="text-subtle">—</span> : fmtMoney(i.valueUsd)}</td>85 <td className={td}>86 <ValueSourceBadge item={i} />87 </td>88 </tr>89 );90 })}91 </tbody>92 </Table>93 </Card>94 </div>95 <p className="text-[11px] text-subtle">96 Shared by @{handle}. Values are RareIndex Valuation estimates; <Delta value={c.summary.returnPct} /> return is shown without purchase details. RareIndex does not authenticate items.97 </p>98 </div>99 );100}101